package io.gitee.zhangbinhub.admin.oauth.base

import io.gitee.zhangbinhub.acp.cloud.resource.server.constant.AcpCloudResourceServerConstant
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.oauth2.core.*
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator
import java.security.Principal

interface BaseAuthenticationProvider : AuthenticationProvider {
    fun getAuthorizationBuilder(
        registeredClient: RegisteredClient,
        authorizationGrantType: AuthorizationGrantType,
        clientPrincipal: OAuth2ClientAuthenticationToken,
        principalName: String
    ): OAuth2Authorization.Builder = OAuth2Authorization.withRegisteredClient(registeredClient)
        .authorizationGrantType(authorizationGrantType)
        .attributes { attrs -> attrs[Principal::class.java.name] = clientPrincipal }
        .authorizedScopes(registeredClient.scopes)
        .principalName(principalName)

    @Throws(AuthenticationException::class)
    fun getRegisteredClient(oAuth2ClientAuthenticationToken: OAuth2ClientAuthenticationToken) =
        oAuth2ClientAuthenticationToken.registeredClient ?: run {
            val error = OAuth2Error(
                OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, "The client unauthorized.", null
            )
            throw OAuth2AuthenticationException(error)
        }

    @Throws(AuthenticationException::class)
    fun generateToken(
        registeredClient: RegisteredClient,
        clientPrincipal: OAuth2ClientAuthenticationToken,
        principal: Authentication,
        authorizationGrantType: AuthorizationGrantType,
        authorizationBuilder: OAuth2Authorization.Builder,
        tokenGenerator: OAuth2TokenGenerator<out OAuth2Token?>,
        userAuthorities: Set<GrantedAuthority>,
        tokenClaimsUserinfo: String,
        additionalParameters: Map<String, Any>
    ): OAuth2AccessTokenAuthenticationToken {
        val tokenContextBuilder = DefaultOAuth2TokenContext.builder().registeredClient(registeredClient)
            .principal(principal)
            .authorizationServerContext(AuthorizationServerContextHolder.getContext())
            .authorizedScopes(registeredClient.scopes)
            .authorizationGrantType(authorizationGrantType)
            .authorizationGrant(principal)
        val accessToken = generateAccessToken(
            tokenContextBuilder,
            tokenGenerator,
            authorizationBuilder,
            userAuthorities,
            tokenClaimsUserinfo
        )
        val refreshToken = generateRefreshToken(
            registeredClient,
            clientPrincipal,
            tokenContextBuilder,
            tokenGenerator,
            authorizationBuilder
        )
        return OAuth2AccessTokenAuthenticationToken(
            registeredClient,
            clientPrincipal,
            accessToken,
            refreshToken,
            additionalParameters
        ).apply {
            this.isAuthenticated = userAuthorities.isNotEmpty() || accessToken.scopes.isNotEmpty()
        }
    }

    @Throws(AuthenticationException::class)
    fun generateAccessToken(
        tokenContextBuilder: DefaultOAuth2TokenContext.Builder,
        tokenGenerator: OAuth2TokenGenerator<out OAuth2Token?>,
        authorizationBuilder: OAuth2Authorization.Builder,
        userAuthorities: Set<GrantedAuthority>,
        tokenClaimsUserinfo: String
    ): OAuth2AccessToken {
        val tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build()
        return tokenGenerator.generate(tokenContext).let { generatedAccessToken ->
            if (generatedAccessToken == null) {
                val error = OAuth2Error(
                    OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the access token.", null
                )
                throw OAuth2AuthenticationException(error)
            }
            OAuth2AccessToken(
                OAuth2AccessToken.TokenType.BEARER,
                generatedAccessToken.tokenValue,
                generatedAccessToken.issuedAt,
                generatedAccessToken.expiresAt,
                tokenContext.authorizedScopes
            ).apply {
                if (generatedAccessToken is ClaimAccessor) {
                    val claims = (generatedAccessToken as ClaimAccessor).claims.toMutableMap()
                    claims[AcpCloudResourceServerConstant.TOKEN_CLAIMS_AUTHORITIES] =
                        userAuthorities.map { item -> item.authority }.toSet()
                    // 添加附加的用户信息 start
                    claims[AcpCloudResourceServerConstant.TOKEN_CLAIMS_USER_INFO] = tokenClaimsUserinfo
                    // 添加附加的用户信息 end
                    authorizationBuilder.token(this) { metadata ->
                        metadata[OAuth2Authorization.Token.CLAIMS_METADATA_NAME] = claims
                    }
                } else {
                    authorizationBuilder.accessToken(this)
                }
            }
        }
    }

    @Throws(AuthenticationException::class)
    fun generateRefreshToken(
        registeredClient: RegisteredClient,
        clientPrincipal: OAuth2ClientAuthenticationToken,
        tokenContextBuilder: DefaultOAuth2TokenContext.Builder,
        tokenGenerator: OAuth2TokenGenerator<out OAuth2Token?>,
        authorizationBuilder: OAuth2Authorization.Builder
    ): OAuth2RefreshToken? =
        if (registeredClient.authorizationGrantTypes.contains(AuthorizationGrantType.REFRESH_TOKEN) && clientPrincipal.clientAuthenticationMethod != ClientAuthenticationMethod.NONE) {
            tokenGenerator.generate(tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build())
                ?.let { generatedRefreshToken ->
                    if (generatedRefreshToken !is OAuth2RefreshToken) {
                        throw OAuth2AuthenticationException(
                            OAuth2Error(
                                OAuth2ErrorCodes.SERVER_ERROR,
                                "The token generator failed to generate the refresh token.",
                                null
                            )
                        )
                    }
                    generatedRefreshToken
                }?.apply {
                    authorizationBuilder.refreshToken(this)
                }
        } else {
            null
        }
}